package diplomovka.filesLoader;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.JavaClass;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;

public class JarFilesLoader {

	private List<String> notFoundedFiles;
	private List<JavaClass> loadedByteCodes;

	public JarFilesLoader() {
		super();
		notFoundedFiles = new ArrayList<String>();
		loadedByteCodes = new ArrayList<JavaClass>();
	}

	public List<JavaClass> getLoadedByteCodes() {
		return loadedByteCodes;
	}

	public List<String> getNotFoundedFiles() {
		return notFoundedFiles;
	}

	/**
	 * Loads all .class files from given jar files and from all files, which are
	 * referenced by given files
	 * 
	 * @param jarFilesNames
	 *            names of jar files, which should be loaded
	 */
	public List<Class> loadClassesFromJarFiles(List<String> jarFilesNames) {
		List<Class> loadedClassesList = new ArrayList<Class>();

		// runs through all given jar files
		for (String jarFileName : jarFilesNames) {
			// load given file
			File file = new File(jarFileName);
			// create jar file from given file
			try {
				JarFile jarFile = new JarFile(file);
				Enumeration<JarEntry> jarFileEntries = jarFile.entries();
				// runs through all entries in jar file
				while (jarFileEntries.hasMoreElements()) {
					JarEntry jarEntry = (JarEntry) jarFileEntries.nextElement();
					String jarEntryName = jarEntry.getName();
					// determine whether it is .class file or not
					boolean isClassFile = jarEntryName.endsWith(".class");
					if (isClassFile) {
						try {
							jarEntryName = jarEntryName.replace('/', '.');
							int extensionBeginIndex = jarEntryName
									.lastIndexOf(".class");
							jarEntryName = jarEntryName.substring(0,
									extensionBeginIndex);

							String nameAsURL = "file:///" + jarFileName;
							URL url = new URL(nameAsURL);
							URL[] urls = new URL[] { url };
							URLClassLoader classLoader = new URLClassLoader(
									urls);
							Class loadedClass = classLoader
									.loadClass(jarEntryName);
							ClassParser parser = new ClassParser(jarFileName,
									jarEntryName.replace('.', '/').concat(
											".class"));
							JavaClass loadedByteCode = parser.parse();
							loadedByteCodes.add(loadedByteCode);

							loadedClassesList.add(loadedClass);
						} catch (ClassNotFoundException ex) {
							System.out
									.println("Can't load class file from jar ["
											+ jarEntryName + "]."
											+ ex.getMessage());
						}
					}
				}

				// add classes from all referenced jar files
				List<Class> classesFromReferencedFiles = loadClassesFromReferencedJarFiles(jarFile);
				loadedClassesList.addAll(classesFromReferencedFiles);
			} catch (IOException e) {
				if (!notFoundedFiles.contains(jarFileName)) {
					notFoundedFiles.add(jarFileName);
				}
				System.out.println("Can't process given file as jar file ["
						+ jarFileName + "].");
			}
		}

		return loadedClassesList;
	}

	/**
	 * Loads .class files from referenced jar files of given jar file
	 * 
	 * @return
	 */
	private List<Class> loadClassesFromReferencedJarFiles(JarFile jarFile) {
		try {
			//get home directory of referencing jar file
			//this directory should be added to the referenced jar file path
			String homeDirectory = jarFile.getName();
			int homeDirectoryEndIndex = homeDirectory.lastIndexOf("\\");
			if (homeDirectoryEndIndex > -1) {
				homeDirectory = homeDirectory.substring(0,
						homeDirectoryEndIndex);
			}

			Manifest manifest = jarFile.getManifest();
			Attributes classPathAttributes = manifest.getMainAttributes();
			String classPathValue = classPathAttributes.getValue("Class-Path");
			// get list of referenced jar files
			if (classPathValue != null && !"".equals(classPathValue)) {
				String[] referencedFiles = classPathValue.split(";");
				List<String> referencedFilesList = new ArrayList<String>();
				for (String referencedFile : referencedFiles) {
					referencedFilesList.add(homeDirectory + File.separator
							+ referencedFile);
				}

				return loadClassesFromJarFiles(referencedFilesList);
			}
		} catch (IOException e) {
			System.out.println("Can't load jar file manifest ["
					+ jarFile.getName() + "].");
		}

		return new ArrayList<Class>();
	}

	/**
	 * Loads all .xml files from given jar files and from all files, which are
	 * referenced by given files
	 * 
	 * @param jarFilesNames
	 *            names of jar files, which should be loaded
	 */
	public Map<String, Element> loadConfigsFromJarFiles(List<String> jarFilesNames) {
		Map<String, Element> loadedConfigsList = new HashMap<String, Element>();

		// runs through all given jar files
		for (String jarFileName : jarFilesNames) {
			// load given file
			File file = new File(jarFileName);
			// create jar file from given file
			try {
				JarFile jarFile = new JarFile(file);
				Enumeration<JarEntry> jarFileEntries = jarFile.entries();
				// runs through all entries in jar file
				while (jarFileEntries.hasMoreElements()) {
					JarEntry jarEntry = (JarEntry) jarFileEntries.nextElement();
					String jarEntryName = jarEntry.getName();
					if (isAllowedInputFile(jarEntryName, jarFileName, jarFile, jarEntry)) {					
						loadedConfigsList.put(jarEntryName,loadRootElementOfTheFile(jarFileName, jarEntryName, jarFile, jarEntry));
					}
				}

				// add configs from all referenced jar files
				Map<String, Element> configsFromReferencedFiles = loadConfigsFromReferencedJarFiles(jarFile);
				loadedConfigsList.putAll(configsFromReferencedFiles);
			} catch (IOException e) {
				if (!notFoundedFiles.contains(jarFileName)) {
					notFoundedFiles.add(jarFileName);
				}
				System.out.println("Can't process given file as jar file ["
						+ jarFileName + "].");
				System.out.println("Reason: ");
				e.printStackTrace();
			}
		}

		return loadedConfigsList;
	}

	/**
	 * Loads .xml files from referenced jar files of given jar file
	 * 
	 * @return
	 */
	private Map<String, Element> loadConfigsFromReferencedJarFiles(JarFile jarFile) {
		try {
			Manifest manifest = jarFile.getManifest();
			Attributes classPathAttributes = manifest.getMainAttributes();
			String classPathValue = classPathAttributes.getValue("Class-Path");
			// get list of referenced jar files
			if (classPathValue != null && !"".equals(classPathValue)) {
				String[] referencedFiles = classPathValue.split(";");
				List<String> referencedFilesList = new ArrayList<String>();
				for (String referencedFile : referencedFiles) {
					referencedFilesList.add(referencedFile);
				}

				return loadConfigsFromJarFiles(referencedFilesList);
			}
		} catch (IOException e) {
			System.out.println("Can't load jar file manifest ["
					+ jarFile.getName() + "].");
		}

		return new HashMap<String, Element>();
	}
	
	/**
	 * Determines whether input file is one of the allowed for the application.
	 * Allowed configuration files are:
	 * - Hibernate configuration file
	 * - Spring configuration file
	 * - EJB deploy file
	 * - Web service deploy file
	 * - XML schema definition file
	 * @param fileName
	 * 			processed file
	 * @param jarFileName
	 * 			name of the jar file, where the loaded file is stored
	 * @param jarFile
	 * 			jar file where the file is stored
	 * @param jarEntry
	 * 			jar entry representing the loaded file
	 * @return true if the file is allowed for the process, else false
	 */ 
	private boolean isAllowedInputFile(String fileName, String jarFileName, JarFile jarFile, JarEntry jarEntry) {
		if (isHibernateConfigFile(fileName))
			return true;
		if (isSpringConfigFile(jarFileName, fileName, jarFile, jarEntry))
			return true;
		if (isEJBConfigFile(jarFileName, fileName, jarFile, jarEntry))
			return true;
		if (isWebServiceConfigFile(jarFileName, fileName, jarFile, jarEntry))
			return true;
		if (isXSDConfigFile(jarFileName, fileName, jarFile, jarEntry))
			return true;
		
		return false;
	}
	
	/**
	 * Checks whether the file is Hibernate configuration file.
	 * @param fileName
	 * 			processed file 
	 * @return true if the file is Hibernate configuration file.
	 */
	private boolean isHibernateConfigFile(String fileName) {
		return fileName.endsWith(".hbm.xml");
	}
	
	/**
	 * Checks whether the file is Spring configuration file.
	 * @param fileName
	 * 			processed file 
	 * @return true if the file is Spring configuration file.
	 */
	private boolean isSpringConfigFile(String jarFileName, String fileName, JarFile jarFile, JarEntry jarEntry) {
		if (! fileName.endsWith(".xml"))
			return false;
			
		Element rootElement = loadRootElementOfTheFile(jarFileName, fileName, jarFile, jarEntry);
		return rootElement.getName().equals("beans");
	}
	
	/**
	 * Checks whether the file is EJB deployment descriptor file.
	 * @param fileName
	 * 			processed file 
	 * @return true if the file is EJB deployment descriptor file.
	 */
	private boolean isEJBConfigFile(String jarFileName, String fileName, JarFile jarFile, JarEntry jarEntry) {
		if (! fileName.equals("ejb-jar.xml"))
			return false;
		
		Element rootElement = loadRootElementOfTheFile(jarFileName, fileName, jarFile, jarEntry);
		return rootElement.getName().equals("ejb-jar");
	}
	
	/**
	 * Checks whether the file is web service configuration file (wsdl or wsdd).
	 * @param fileName
	 * 			processed file 
	 * @return true if the file is web service configuration file.
	 */
	private boolean isWebServiceConfigFile(String jarFileName, String fileName, JarFile jarFile, JarEntry jarEntry) {
		if ((! fileName.endsWith(".wsdl")) && (! fileName.endsWith(".wsdd")))
			return false;
		
		Element rootElement = loadRootElementOfTheFile(jarFileName, fileName, jarFile, jarEntry);
		boolean isWsdl = rootElement.getName().equals("definitions");
		boolean isWsdd = rootElement.getName().equals("deployment");
		return isWsdd || isWsdl;		
	}
	
	/**
	 * Checks whether the file is XML schema definition file.
	 * @param fileName
	 * 			processed file 
	 * @return true if the file is XML schema definition file.
	 */
	private boolean isXSDConfigFile(String jarFileName, String fileName, JarFile jarFile, JarEntry jarEntry) {
		if (! fileName.endsWith(".xsd"))
			return false;
		
		Element rootElement = loadRootElementOfTheFile(jarFileName, fileName, jarFile, jarEntry);
		return rootElement.getName().equals("schema");
	}
	
	/**
	 * Loads root element of the file.
	 * @param jarFileName
	 * 			name of the jar file, where the loaded file is stored
	 * @param configFileName
	 * 			name of the loaded file
	 * @param jarFile
	 * 			jar file where the file is stored
	 * @param jarEntry
	 * 			jar entry representing the loaded file
	 * @return root element of the file or null if the couldn't be loaded
	 */
	private Element loadRootElementOfTheFile(String jarFileName, String configFileName, JarFile jarFile, JarEntry jarEntry) {
		try {
			String nameAsURL = "file:///" + jarFileName;
			URL url = new URL(nameAsURL);
			URL[] urls = new URL[] { url };
			URLClassLoader classLoader = new URLClassLoader(urls);
			URL loadedConfigURL = classLoader
					.findResource(configFileName);
			File loadedConfig = new File(loadedConfigURL.getPath());
			InputStream is = jarFile.getInputStream(jarEntry);
			SAXBuilder builder = new SAXBuilder();
			Document doc = builder.build(is);
			Element rootElement = doc.getRootElement();
			
			return rootElement;
		} catch (MalformedURLException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (JDOMException e) {
			e.printStackTrace();
		}
		
		return null;
	}
}
